Skip to content

feat: add global server power control mutations#1970

Closed
elibosley wants to merge 4 commits intomainfrom
feat/server-power-global
Closed

feat: add global server power control mutations#1970
elibosley wants to merge 4 commits intomainfrom
feat/server-power-global

Conversation

@elibosley
Copy link
Copy Markdown
Member

@elibosley elibosley commented Mar 27, 2026

Summary

Adds GraphQL mutations for server reboot and shutdown, replacing the legacy form-based Boot.php approach. These mutations are now available globally (not scoped to onboarding), gated behind UPDATE_ANY CONFIG permissions.

Server Power API:

  • New ServerPowerService with reboot() and shutdown() methods calling /sbin/reboot -n and /sbin/poweroff -n
  • New ServerPowerMutationsResolver as nested resolver under Mutation.serverPower
  • Registered in resolvers.module.ts with proper NestJS DI

Frontend (Onboarding):

  • Replaced legacy form POST to /plugins/dynamix/include/Boot.php with proper GraphQL mutations
  • Added noRetry context to prevent Apollo RetryLink from re-triggering power actions when the connection drops during reboot/shutdown
  • New serverPower.mutation.ts with ServerReboot and ServerShutdown mutations

Housekeeping:

  • Removed server lifecycle restriction from AGENTS.md
  • Regenerated GraphQL schema and codegen types

Test Coverage

All new code paths have test coverage:

  • server-power.service.spec.ts: 4 tests (happy path + error for reboot/shutdown)
  • internalBoot.test.ts: 12 tests covering reboot, shutdown, create, BIOS parsing, error paths
  • Tests verify noRetry context is passed to prevent retry-triggered power actions

Pre-Landing Review

Codex structured review found 1 P1 (Apollo RetryLink retrying power mutations). Fixed by adding context: { noRetry: true } to both mutation calls.

Test plan

  • All API tests pass (175 files, 1970 tests)
  • All web tests pass (64 files, 635 tests)
  • Codex adversarial review completed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Server power management: initiate server reboot and shutdown directly from the app (subject to authorization).
    • Encrypted array startup: supply decryption credentials (password or keyfile) when starting encrypted arrays to simplify unlocking.
    • Onboarding: internal boot actions updated to use the new server power operations.
  • Chores

    • Updated application version to 4.31.1.

elibosley and others added 3 commits March 27, 2026 11:09
New GraphQL mutations for server reboot and shutdown, replacing the
legacy form-based Boot.php approach. Gated behind UPDATE_ANY CONFIG
permissions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace legacy form POST to Boot.php with proper GraphQL mutations.
Adds noRetry context to prevent Apollo RetryLink from re-triggering
power actions when the connection drops during reboot/shutdown.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9990275f-d012-4d3a-b282-c7b034323de6

📥 Commits

Reviewing files that changed from the base of the PR and between 4a910a6 and 79cdcb7.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts

Walkthrough

This pull request adds GraphQL mutations for server power control (reboot and shutdown), implements NestJS resolvers and a service that execute system power commands, extends the GraphQL ArrayStateInput with decryption credentials, migrates the web client's internal boot handler from HTML form submission to GraphQL mutations, and updates generated types and the API version.

Changes

Cohort / File(s) Summary
Agent Instructions & Config
AGENTS.md, api/dev/configs/api.json
Removed an agent instruction line about server lifecycle handling; bumped API version from 4.29.2 to 4.31.1.
GraphQL Schema
api/generated-schema.graphql
Added decryptionPassword and decryptionKeyfile to ArrayStateInput; added ServerPowerMutations type with reboot and shutdown; exposed via root Mutation.
Backend: Mutation Models & Resolvers
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts, api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
Added ServerPowerMutations class and serverPower root mutation, plus resolver method returning the new mutation group.
Backend: Server Power Resolver & Module
api/src/unraid-api/graph/resolvers/server-power/server-power.mutations.resolver.ts, api/src/unraid-api/graph/resolvers/resolvers.module.ts
Added ServerPowerMutationsResolver with permission-gated reboot and shutdown resolve fields and registered resolver/service providers in the resolvers module.
Backend: Service & Tests
api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts, api/src/unraid-api/graph/resolvers/server-power/server-power.service.spec.ts
New ServerPowerService executing /sbin/reboot -n and /sbin/poweroff -n via execa; added Vitest tests mocking execa to verify success and propagation of errors. Review for privileged command use is required.
Frontend: GraphQL Documents & Types
web/src/components/Onboarding/graphql/serverPower.mutation.ts, web/src/composables/gql/gql.ts, web/src/composables/gql/graphql.ts
Added ServerReboot and ServerShutdown mutation documents and typed definitions; extended generated types with ServerPowerMutations and added ArrayStateInput decryption fields.
Frontend: Internal Boot Handler & Tests
web/src/components/Onboarding/composables/internalBoot.ts, web/__test__/components/Onboarding/internalBoot.test.ts
Replaced dynamic HTML form POST approach with async Apollo GraphQL mutations for reboot/shutdown (no-cache, noRetry context); updated tests to assert mutation calls.

Sequence Diagram

sequenceDiagram
    participant Client as Web Client
    participant GraphQL as GraphQL API
    participant Resolver as ServerPowerMutationsResolver
    participant Service as ServerPowerService
    participant System as System (reboot/poweroff)

    Client->>GraphQL: mutation ServerReboot / ServerShutdown
    GraphQL->>Resolver: resolve field (permission check)
    Resolver->>Service: call reboot() / shutdown()
    Service->>System: execa('/sbin/reboot' or '/sbin/poweroff', ['-n'])
    System-->>Service: command result
    Service-->>Resolver: returns Promise<boolean>
    Resolver-->>GraphQL: field result
    GraphQL-->>Client: { data: { serverPower: { reboot|shutdown: true } } }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I tapped the keys and gave a hop,
Mutations hum and circuits stop,
No paper forms — a cleaner beat,
Reboot, shutdown — a rabbit's feat! ⚡

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add global server power control mutations' accurately describes the main change: adding new GraphQL mutations for server reboot and shutdown functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/server-power-global

Comment @coderabbitai help to get the list of available commands and usage tips.

@elibosley elibosley requested a review from Ajit-Mehrotra March 27, 2026 15:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a910a6f35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +231 to +233
export const submitInternalBootReboot = async () => {
const apolloClient = useApolloClient().client;
await apolloClient.mutate({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle power-mutation promise failures in helper

These helpers now return rejected promises on GraphQL/network errors, but the existing callers still invoke them fire-and-forget (OnboardingNextStepsStep.vue and OnboardingInternalBoot.standalone.vue), so a failed reboot/shutdown request becomes an unhandled rejection and leaves the onboarding flow in a non-recoverable loading path instead of surfacing an error or fallback. Please either swallow/normalize expected disconnect errors inside these helpers or make callers await and handle failures.

Useful? React with 👍 / 👎.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 78.04878% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.10%. Comparing base (9323b14) to head (79cdcb7).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...rs/server-power/server-power.mutations.resolver.ts 47.82% 12 Missing ⚠️
...aid-api/graph/resolvers/mutation/mutation.model.ts 55.55% 4 Missing ⚠️
...-api/graph/resolvers/mutation/mutation.resolver.ts 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1970      +/-   ##
==========================================
- Coverage   52.12%   52.10%   -0.03%     
==========================================
  Files        1031     1034       +3     
  Lines       71589    71624      +35     
  Branches     8102     8096       -6     
==========================================
  Hits        37319    37319              
- Misses      34145    34180      +35     
  Partials      125      125              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts (1)

9-19: Consider wrapping execa calls in try/catch for consistent error handling.

The existing ups.service.ts pattern wraps execa calls with error handling to avoid exposing raw OS-level error messages to GraphQL clients. If these commands fail (e.g., permission denied, binary not found), the current implementation would propagate the raw error.

♻️ Suggested error handling pattern (based on ups.service.ts)
     async reboot(): Promise<boolean> {
         this.logger.log('Server reboot requested via GraphQL');
-        await execa('/sbin/reboot', ['-n']);
-        return true;
+        try {
+            await execa('/sbin/reboot', ['-n']);
+            return true;
+        } catch (error) {
+            this.logger.error('Failed to initiate server reboot:', error);
+            throw new Error(
+                `Failed to initiate server reboot: ${error instanceof Error ? error.message : String(error)}`
+            );
+        }
     }

     async shutdown(): Promise<boolean> {
         this.logger.log('Server shutdown requested via GraphQL');
-        await execa('/sbin/poweroff', ['-n']);
-        return true;
+        try {
+            await execa('/sbin/poweroff', ['-n']);
+            return true;
+        } catch (error) {
+            this.logger.error('Failed to initiate server shutdown:', error);
+            throw new Error(
+                `Failed to initiate server shutdown: ${error instanceof Error ? error.message : String(error)}`
+            );
+        }
     }

Based on learnings: "In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts`
around lines 9 - 19, The reboot() and shutdown() methods call execa directly and
can throw raw OS errors to GraphQL clients; wrap the execa('/sbin/reboot',
['-n']) in reboot() and execa('/sbin/poweroff', ['-n']) in shutdown() with
try/catch, log the error via this.logger.error and throw a controlled error (or
return false) consistent with the pattern used in ups.service.ts so the service
handles failures instead of exposing raw exceptions to resolvers; ensure you
reference the reboot and shutdown methods and follow the same error message
format and return semantics as ups.service.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/Onboarding/composables/internalBoot.ts`:
- Around line 231-247: The helpers submitInternalBootReboot and
submitInternalBootShutdown currently expose rejected Promises when the server
drops connection; either require callers to await them or make them truly
fire-and-forget—change the functions (submitInternalBootReboot and
submitInternalBootShutdown) to wrap the apolloClient.mutate call in a try/catch
and swallow or log errors (e.g., process/console logging) so the returned
Promise never rejects, preserving the fire-and-forget contract used by
OnboardingNextStepsStep.vue and OnboardingInternalBoot.standalone.vue.

---

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts`:
- Around line 9-19: The reboot() and shutdown() methods call execa directly and
can throw raw OS errors to GraphQL clients; wrap the execa('/sbin/reboot',
['-n']) in reboot() and execa('/sbin/poweroff', ['-n']) in shutdown() with
try/catch, log the error via this.logger.error and throw a controlled error (or
return false) consistent with the pattern used in ups.service.ts so the service
handles failures instead of exposing raw exceptions to resolvers; ensure you
reference the reboot and shutdown methods and follow the same error message
format and return semantics as ups.service.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a00898ec-f07f-40f8-9cc3-66a6ef6ab9fb

📥 Commits

Reviewing files that changed from the base of the PR and between 29f814b and 4a910a6.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (14)
  • AGENTS.md
  • api/dev/configs/api.json
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/server-power/server-power.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/server-power/server-power.service.spec.ts
  • api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts
  • web/__test__/components/Onboarding/internalBoot.test.ts
  • web/src/components/Onboarding/composables/internalBoot.ts
  • web/src/components/Onboarding/graphql/serverPower.mutation.ts
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment on lines +231 to +247
export const submitInternalBootReboot = async () => {
const apolloClient = useApolloClient().client;
await apolloClient.mutate({
mutation: SERVER_REBOOT_MUTATION,
fetchPolicy: 'no-cache',
context: { noRetry: true },
});
};

export const submitInternalBootReboot = () => submitBootCommand('reboot');
export const submitInternalBootShutdown = () => submitBootCommand('shutdown');
export const submitInternalBootShutdown = async () => {
const apolloClient = useApolloClient().client;
await apolloClient.mutate({
mutation: SERVER_SHUTDOWN_MUTATION,
fetchPolicy: 'no-cache',
context: { noRetry: true },
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 '\bsubmitInternalBoot(Reboot|Shutdown)\s*\(' web/src/components/Onboarding

Repository: unraid/api

Length of output: 1615


🏁 Script executed:

#!/bin/bash
# Check the actual functions at lines 231-247 to confirm they are async
sed -n '225,255p' web/src/components/Onboarding/composables/internalBoot.ts

Repository: unraid/api

Length of output: 606


🏁 Script executed:

#!/bin/bash
# Check the caller contexts in both Vue files for error handling or other patterns
echo "=== OnboardingNextStepsStep.vue context ===" 
sed -n '125,150p' web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue

echo -e "\n=== OnboardingInternalBoot.standalone.vue context ==="
sed -n '55,75p' web/src/components/Onboarding/standalone/OnboardingInternalBoot.standalone.vue

Repository: unraid/api

Length of output: 1353


Keep these power helpers fire-and-forget, or await them everywhere.

noRetry avoids duplicate mutations, but a successful reboot/shutdown can still make Apollo reject once the server drops the connection before sending a response. These helpers now return a Promise from async functions, while all current callers in OnboardingNextStepsStep.vue (lines 135, 140) and OnboardingInternalBoot.standalone.vue (lines 64, 66) invoke them without await. If the mutation fails, the rejection goes unhandled and the onboarding flow has no recovery path.

One way to preserve the previous fire-and-forget contract
-export const submitInternalBootReboot = async () => {
+export const submitInternalBootReboot = (): void => {
   const apolloClient = useApolloClient().client;
-  await apolloClient.mutate({
-    mutation: SERVER_REBOOT_MUTATION,
-    fetchPolicy: 'no-cache',
-    context: { noRetry: true },
-  });
+  void apolloClient
+    .mutate({
+      mutation: SERVER_REBOOT_MUTATION,
+      fetchPolicy: 'no-cache',
+      context: { noRetry: true },
+    })
+    .catch(() => undefined);
 };
 
-export const submitInternalBootShutdown = async () => {
+export const submitInternalBootShutdown = (): void => {
   const apolloClient = useApolloClient().client;
-  await apolloClient.mutate({
-    mutation: SERVER_SHUTDOWN_MUTATION,
-    fetchPolicy: 'no-cache',
-    context: { noRetry: true },
-  });
+  void apolloClient
+    .mutate({
+      mutation: SERVER_SHUTDOWN_MUTATION,
+      fetchPolicy: 'no-cache',
+      context: { noRetry: true },
+    })
+    .catch(() => undefined);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/composables/internalBoot.ts` around lines 231 -
247, The helpers submitInternalBootReboot and submitInternalBootShutdown
currently expose rejected Promises when the server drops connection; either
require callers to await them or make them truly fire-and-forget—change the
functions (submitInternalBootReboot and submitInternalBootShutdown) to wrap the
apolloClient.mutate call in a try/catch and swallow or log errors (e.g.,
process/console logging) so the returned Promise never rejects, preserving the
fire-and-forget contract used by OnboardingNextStepsStep.vue and
OnboardingInternalBoot.standalone.vue.

@github-actions
Copy link
Copy Markdown
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1970/dynamix.unraid.net.plg

@elibosley elibosley closed this Mar 27, 2026
@elibosley elibosley deleted the feat/server-power-global branch March 27, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant